package tfsIssueTracker;

import com.intellij.openapi.diagnostic.Logger;
import jetbrains.buildServer.issueTracker.AbstractIssueProvider;
import jetbrains.buildServer.issueTracker.IssueFetcher;
import jetbrains.buildServer.issueTracker.IssueMention;
import jetbrains.buildServer.issueTracker.IssueProviderType;
import jetbrains.buildServer.util.cache.EhCacheUtil;
import jetbrains.buildServer.vcs.VcsModification;
import net.sf.ehcache.Cache;
import org.jetbrains.annotations.NotNull;
import tfsIssueTracker.commands.FindIssueMentionsByModificationCommand;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collection;

public class TfsIssueProvider extends AbstractIssueProvider {
    private final TfsExeWrapper wrapper;
    private Logger LOG = Logger.getInstance(TfsIssueProvider.class.getName());
    private Cache myCache;

    public TfsIssueProvider(@NotNull final IssueProviderType type, @NotNull IssueFetcher fetcher, TfsExeWrapper wrapper, @NotNull EhCacheUtil cacheUtil) {
        super(type.getType(), fetcher);
        this.wrapper = wrapper;
        myCache = cacheUtil.createCache("issueMentions");
    }

    @Override
    protected boolean useIdPrefix() {
        return false;
    }

    @NotNull
    @Override
    public Collection<IssueMention> getRelatedIssues(@NotNull final VcsModification modification) {
        final VcsModification myModification = modification;
        Collection<IssueMention> result = new ArrayList<IssueMention>();

        // because we assume that TFS is used for version control
        // then personal builds cannot carry issue information because
        // they are implemented by using shelvesets by TC
        // If we wanted to support TFS for issue tracking with a different
        // VCS then we could parse the issue id in the commit message
        // and support personal builds as well
        if(modification.isPersonal()) {
            return result;
        }

        try {
            Collection<SerializableIssueMention> issueMentions = getFromCacheOrFetch(modification.getId(), new FetchFunction() {
                @NotNull
                public Collection<SerializableIssueMention> fetch() throws Exception {
                    String revisionAfterChange = myModification.getChanges().get(0).getAfterChangeRevisionNumber();

                    LOG.debug(String.format("Issue mentions for TeamCity modification id %s, VCS revision %s, were not in cache, retrieving from source",
                            myModification.getId(), revisionAfterChange));

                    return new FindIssueMentionsByModificationCommand(revisionAfterChange, Settings.create(myHost, myCredentials, wrapper)).execute();
                }
            });

            for(SerializableIssueMention mention: issueMentions) {
                result.add(new IssueMention(mention.getId(), mention.getUrl()));
            }
        } catch (Exception e) {
            LOG.debug(e);
            LOG.error(e);
        }

        return result;
    }

    @NotNull
  protected Collection<SerializableIssueMention> getFromCacheOrFetch(@NotNull Object key, @NotNull FetchFunction function)
      throws Exception {
    // NOTE: UGLY HACK!
    // EhCache uses 'Thread.currentThread().getContextClassLoader()' to load classes of serialized
    // instances, which is not the same as 'getClass().getClassLoader()'. This causes a problem
    // for plugin classes, such as IssueDataImpl.
    // If you can make this work without the hack, you're welcome to fix it.

    ClassLoader currentLoader = Thread.currentThread().getContextClassLoader();
    Thread.currentThread().setContextClassLoader(getClass().getClassLoader());

    try {
      net.sf.ehcache.Element element = myCache.get(key);
      if (element != null) {
        Serializable value = element.getValue();
        if (value == null) {
          throw new RuntimeException("Cached issue mention for " + key + " is null " +
                                     "(probably failed to fetch it previous time)");
        }
        return (Collection<SerializableIssueMention>) value;
      }

      try {
        Collection<SerializableIssueMention> result = function.fetch();
        myCache.put(new net.sf.ehcache.Element(key, result));
        return result;
      } catch (Exception e) {
        myCache.put(new net.sf.ehcache.Element(key, null));
        throw e;
      }
    } finally {
      Thread.currentThread().setContextClassLoader(currentLoader);
    }
  }

  /**
   * An interface represents an action of actual issue fetching.
   * This action takes place when a suitable issue isn't found in cache, or expired.
   */
  protected static interface FetchFunction {
    /**
     * Fetches the issue. Throws an exception in case of a problem.
     * @return a non-null issue in case of success
     * @throws Exception in case of an error
     */
    @NotNull
    Collection<SerializableIssueMention> fetch() throws Exception;
  }
}
